<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="apple-touch-icon" href="apple-touch-icon.png">
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="manifest" href="a3.webmanifest">
    <title>A3</title>

    <script>
      var simScale = 1;
      function setSimScale(scale){
        simScale = scale;
      }
    </script>
  </head>
  <body>
    <link rel="stylesheet" href="css/main.css"/>
    <script src="js/Theme.js"></script>
    <script src="js/ammo.js/build/ammo.wasm.js"></script>
    <div id="container"></div>
    <div class="side">
      <label>位置</label><div class="monitor" id="monitor_pos"></div>
      <label>线速度</label><div class="monitor" id="monitor_lv"></div>
      <label>角速度</label><div class="monitor" id="monitor_av"></div>
    </div>
    <script type="module">
      import * as THREE from "./js/three.js/build/three.module.js";
      import Stats from './jsm/libs/stats.module.js';
		  import { OrbitControls } from './jsm/controls/OrbitControls.js';
      import { GUI } from './jsm/libs/dat.gui.module.js';
      import { Line2 } from './jsm/lines/Line2.js';
			import { LineMaterial } from './jsm/lines/LineMaterial.js';
			import { LineGeometry } from './jsm/lines/LineGeometry.js';
      
      // Simulation
      let isStop  = true;
      let isPause = false;
      
      // Graphics variables
      let monitorPos, monitorAV, monitorLV;
      let container, stats;
      let camera, controls, scene, renderer;
      let cameraMonitor, sceneMonitor, monitorTime, updateDataIndex;
      let sceneMonitorLV, sceneMonitorAV; 
      let monitorPosX, monitorPosY, monitorPosZ, monitorLVX,monitorLVY,monitorLVZ,monitorAVX,monitorAVY,monitorAVZ;
      let positionLVX, positionLVY, positionLVZ;
      let positionAVX, positionAVY, positionAVZ;
      let datPosX, datPosY, datPosZ, cPosX, cPosY, cPosZ;
      let datLVX, datLVY, datLVZ;
      let datAVX, datAVY, datAVZ;

      let rendererPos,rendererAV,rendererLV;
      let cameraPos, cameraAV, cameraLV;
      let scenePos, sceneAV, sceneLV;
      let gridPos, gridAV, gridLV;

      let textureLoader;
      const clock = new THREE.Clock();

      // Physics variables
      const gravityConstant = - 9.8;
      let collisionConfiguration;
      let dispatcher;
      let broadphase;
      let solver;
      let softBodySolver;
      let physicsWorld;
      const rigidBodies = [];
      const margin = 0.05;
      let hinge;
      let rope;
      let transformAux1;

      let armMovement = 0;

      let audio;
      let ping_pong;
      let table;
      
      // Ball
      const ballMass = 0.026;
      const ballRadius = 0.02;
      let ballPosX, ballPosY, ballPosZ;
      let ballLVX, ballLVY, ballLVZ;
      let takeTime;
      const ballMaterailHit = new THREE.MeshPhongMaterial( { color: 0xff0000 } ) ;
      const ballMaterailNormal = new THREE.MeshPhongMaterial( { color: 0xffeeee } ) ;
      const ballMaterailContact= new THREE.MeshPhongMaterial( { color: 0x00ff00 } ) ;

      // table
      const tableHeight = 0.76;
      const tableLength = 2.74;
      const tableWidth  = 1.525;

      // net
      const netHeight = 0.1525;
      const netLength = 0.01;
      const netWidth  = 1.83;

      
      let params = {
        // 位置
        startPositionX:1.5,
        startPositionY:1,
        startPositionZ:0,
        // 线速度 角速度
        startLinearVelocityX:0,//-5,
        startLinearVelocityY:0,//-4.5,
        startLinearVelocityZ:0,

        startAngularVelocityX:0,
        startAngularVelocityY:0,
        startAngularVelocityZ:0,

        // 初始冲量
        startImpulsX:0,
        startImpulsY:0,
        startImpulsZ:0,

        // 发球参数
        serveX:-0.2,
        serveY:0.03,
        serveZ:0,
        servePosX:0,
        servePosY:0,
        servePosZ:0,

        // 球位置信息
        useImpulse:false,

        distance : 0,
        time:0.4,

        force:()=>{
          console.log('球位置:',ping_pong.position);
          // 计算当前冲量 v * mass
          // ping_pong.userData.physicsBody.applyImpulse(
          //   new Ammo.btVector3(params.serveX, params.serveY, params.serveZ),
          //   new Ammo.btVector3(params.servePosX, params.servePosY, params.servePosZ),
          // );
          ping_pong.userData.physicsBody.setAngularVelocity(
            new Ammo.btVector3(100, 0, 0)
          )
        }
      }


      Ammo().then( function ( AmmoLib ) {

        Ammo = AmmoLib;

        init();
        animate();

      } );

      function startSimulate(dt){
        isStop  = false;
        isPause = false;
        monitorTime = 0;

        animate();
      }

      function stopSimulate(dt){
        isStop  = true;
        isPause = false;

        resetPingPong();
      }

      function pauseSimulate(dt){
        isPause = true;
      }

      document.getElementById("start").onclick=startSimulate;
      document.getElementById("stop").onclick=stopSimulate;
      document.getElementById("pause").onclick=pauseSimulate;

      function init() {

        initGraphics();

        initPhysics();

        createObjects();

        initInput();

        setupGUI();

      }

      function initMonitor(){
        updateDataIndex = 0;

        const material = new LineMaterial( { 
          vertexColors: true, 
          linewidth: 0.01, 
					alphaToCoverage: true,
        } );
        // monitor
        monitorPos = document.getElementById( 'monitor_pos' );
        monitorAV = document.getElementById( 'monitor_av' );
        monitorLV = document.getElementById( 'monitor_lv' );
        // position
        cameraPos = new THREE.PerspectiveCamera( 60, monitorPos.clientWidth/ monitorPos.clientHeight, 0.1, 2000 );
        cameraPos.position.set( 0, -100, 0 );
        cameraPos.lookAt(0, 0, 0);

        scenePos = new THREE.Scene();
        scenePos.background = new THREE.Color( 0xffffff );

        gridPos = new THREE.GridHelper(monitorPos.clientWidth * 0.48, 10);
        scenePos.add(gridPos);
        var xminPos = gridPos.geometry.attributes.position.array[0];

        datPosX = [];
        datPosY = [];
        datPosZ = [];
        cPosX = [];
        cPosY = [];
        cPosZ = [];
        for( let i = xminPos; i <= xminPos*-1; i += 1){
          datPosX.push(i, 0, 0);
          datPosY.push(i, 0, 0);
          datPosZ.push(i, 0, 0);
          cPosX.push(1, 0, 0);
          cPosY.push(0, 1, 0);
          cPosZ.push(0, 0, 1);
        }
        const geometryPosX = new LineGeometry(); 
				geometryPosX.setPositions( datPosX );
				geometryPosX.setColors( cPosX );
				monitorPosX = new Line2( geometryPosX, material );
        monitorPosX.computeLineDistances();

        const geometryPosY = new LineGeometry(); 
				geometryPosY.setPositions( datPosY );
				geometryPosY.setColors( cPosY );
				monitorPosY = new Line2( geometryPosY, material );
        monitorPosY.computeLineDistances();

        const geometryPosZ = new LineGeometry(); 
				geometryPosZ.setPositions( datPosZ );
				geometryPosZ.setColors( cPosZ );
				monitorPosZ = new Line2( geometryPosZ, material );
        monitorPosZ.computeLineDistances();

				scenePos.add( monitorPosX );
        scenePos.add( monitorPosY );
        scenePos.add( monitorPosZ );

        rendererPos = new THREE.WebGLRenderer({ alpha: true });
        // rendererPos.setClearColor(0xFFFFFF, 0);
        rendererPos.setPixelRatio( window.devicePixelRatio );
        rendererPos.setSize( monitorPos.clientWidth, monitorPos.clientHeight );
        rendererPos.shadowMap.enabled = false;
        monitorPos.appendChild( rendererPos.domElement );
        // linear velocity
        cameraLV = new THREE.PerspectiveCamera( 60, monitorLV.clientWidth/ monitorLV.clientHeight, 0.1, 2000 );
        cameraLV.position.set( 0, -100, 0 );
        cameraLV.lookAt(0, 0, 0);

        sceneLV = new THREE.Scene();
        sceneLV.background = new THREE.Color( 0xffffff );

        gridLV = new THREE.GridHelper(monitorLV.clientWidth * 0.48, 10);
        sceneLV.add(gridLV);

        var xminPos = gridLV.geometry.attributes.position.array[0];

        datLVX= [];
        datLVY = [];
        datLVZ = [];
        cPosX = [];
        cPosY = [];
        cPosZ = [];
        for( let i = xminPos; i <= xminPos*-1; i += 1){
          datLVX.push(i, 0, 0);
          datLVY.push(i, 0, 0);
          datLVZ.push(i, 0, 0);
          cPosX.push(1, 0, 0);
          cPosY.push(0, 1, 0);
          cPosZ.push(0, 0, 1);
        }

        const geometryLVX = new LineGeometry(); 
				geometryLVX.setPositions( datLVX );
				geometryLVX.setColors( cPosX );
				monitorLVX = new Line2( geometryLVX, material );
        monitorLVX.computeLineDistances();

        const geometryLVY = new LineGeometry(); 
				geometryLVY.setPositions( datLVY );
				geometryLVY.setColors( cPosY );
				monitorLVY = new Line2( geometryLVY, material );
        monitorLVY.computeLineDistances();

        const geometryLVZ = new LineGeometry(); 
				geometryLVZ.setPositions( datLVZ );
				geometryLVZ.setColors( cPosZ );
				monitorLVZ = new Line2( geometryLVZ, material );
        monitorLVZ.computeLineDistances();

				sceneLV.add( monitorLVX );
        sceneLV.add( monitorLVY );
        sceneLV.add( monitorLVZ );

        rendererLV = new THREE.WebGLRenderer({ alpha: true });
        rendererLV.setPixelRatio( window.devicePixelRatio );
        rendererLV.setSize( monitorLV.clientWidth, monitorLV.clientHeight );
        rendererLV.shadowMap.enabled = false;
        monitorLV.appendChild( rendererLV.domElement );

        // angular velocity
        cameraAV = new THREE.PerspectiveCamera( 60, monitorAV.clientWidth/ monitorAV.clientHeight, 0.1, 2000 );
        cameraAV.position.set( 0, -100, 0 );
        cameraAV.lookAt(0, 0, 0);

        sceneAV = new THREE.Scene();
        sceneAV.background = new THREE.Color( 0xffffff );

        gridAV = new THREE.GridHelper(monitorAV.clientWidth * 0.48, 10);
        sceneAV.add(gridAV);
        
        var xminPos = gridAV.geometry.attributes.position.array[0];

        datAVX= [];
        datAVY = [];
        datAVZ = [];
        cPosX = [];
        cPosY = [];
        cPosZ = [];
        for( let i = xminPos; i <= xminPos*-1; i += 1){
          datAVX.push(i, 0, 0);
          datAVY.push(i, 0, 0);
          datAVZ.push(i, 0, 0);
          cPosX.push(1, 0, 0);
          cPosY.push(0, 1, 0);
          cPosZ.push(0, 0, 1);
        }

        const geometryAVX = new LineGeometry(); 
				geometryAVX.setPositions( datAVX );
				geometryAVX.setColors( cPosX );
				monitorAVX = new Line2( geometryAVX, material );
        monitorAVX.computeLineDistances();

        const geometryAVY = new LineGeometry(); 
				geometryAVY.setPositions( datAVY );
				geometryAVY.setColors( cPosY );
				monitorAVY = new Line2( geometryAVY, material );
        monitorAVY.computeLineDistances();

        const geometryAVZ = new LineGeometry(); 
				geometryAVZ.setPositions( datAVZ );
				geometryAVZ.setColors( cPosZ );
				monitorAVZ = new Line2( geometryAVZ, material );
        monitorAVZ.computeLineDistances();

				sceneAV.add( monitorAVX );
        sceneAV.add( monitorAVY );
        sceneAV.add( monitorAVZ );

        rendererAV = new THREE.WebGLRenderer({ alpha: true });
        rendererAV.setPixelRatio( window.devicePixelRatio );
        rendererAV.setSize( monitorAV.clientWidth, monitorAV.clientHeight );
        rendererAV.shadowMap.enabled = false;
        monitorAV.appendChild( rendererAV.domElement );
      }

      function initGraphics() {

        initMonitor();

        container = document.getElementById( 'container' );
        

        camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.2, 2000 );

        scene = new THREE.Scene();
        scene.background = new THREE.Color( 0xbfd1e5 );

        camera.position.set( - 4, 2, 4 );

        renderer = new THREE.WebGLRenderer({ alpha: true });
        renderer.setClearColor(0XFFFFFF, 0);
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize( window.innerWidth, window.innerHeight );
        renderer.shadowMap.enabled = true;
        container.appendChild( renderer.domElement );

        controls = new OrbitControls( camera, renderer.domElement );
        controls.target.set( 0, 2, 0 );
        controls.update();

        textureLoader = new THREE.TextureLoader();

        const ambientLight = new THREE.AmbientLight( 0xffffff );
        scene.add( ambientLight );

        // Textures

				const loader = new THREE.CubeTextureLoader();
				loader.setPath( 'textures/cube/Bridge2/' );

				const textureCube = loader.load( [ 'posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg' ] );
				textureCube.encoding = THREE.sRGBEncoding;

				// const textureLoader = new THREE.TextureLoader();

				// textureEquirec = textureLoader.load( 'textures/2294472375_24a3b8ef46_o.jpg' );
				// textureEquirec.mapping = THREE.EquirectangularReflectionMapping;
				// textureEquirec.encoding = THREE.sRGBEncoding;

				scene.background = textureCube;

        const light = new THREE.DirectionalLight( 0xffffff, 1 );
        light.position.set( 0, 10, 0 );
        light.castShadow = true;
        const d = 10;
        light.shadow.camera.left = - d;
        light.shadow.camera.right = d;
        light.shadow.camera.top = d;
        light.shadow.camera.bottom = - d;

        light.shadow.camera.near = 2;
        light.shadow.camera.far = 50;

        light.shadow.mapSize.x = 1024;
        light.shadow.mapSize.y = 1024;

        scene.add( light );

        stats = new Stats();
        stats.domElement.style.position = 'absolute';
        stats.domElement.style.top = getComputedStyle(document.documentElement).getPropertyValue('--menubar-height');//document.documentElement.style.getPropertyValue('--menubar-height');
        container.appendChild( stats.domElement );

        //

        window.addEventListener( 'resize', onWindowResize );

      }

      function initPhysics() {

        // Physics configuration

        collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
        dispatcher = new Ammo.btCollisionDispatcher( collisionConfiguration );
        broadphase = new Ammo.btDbvtBroadphase();
        solver = new Ammo.btSequentialImpulseConstraintSolver();
        softBodySolver = new Ammo.btDefaultSoftBodySolver();
        physicsWorld = new Ammo.btSoftRigidDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration, softBodySolver );
        physicsWorld.setGravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
        physicsWorld.getWorldInfo().set_m_gravity( new Ammo.btVector3( 0, gravityConstant, 0 ) );
        physicsWorld.getWorldInfo().set_air_density ( 0.8 );

        transformAux1 = new Ammo.btTransform();

      }

      function resetPingPong(){
        const objPhys = ping_pong.userData.physicsBody;
        transformAux1.setIdentity();
        transformAux1.setOrigin( new Ammo.btVector3( params.startPositionX, params.startPositionY, params.startPositionZ ) );
        transformAux1.setRotation( new Ammo.btQuaternion( 0,0,0,1 ) );
        const motionState = new Ammo.btDefaultMotionState( transformAux1 );

        objPhys.setMotionState(motionState);
        ping_pong.position.set(params.startPositionX, params.startPositionY, params.startPositionZ);
        ping_pong.quaternion.set( 0,0,0,1 );

      }

      function createObjects() {

        const pos = new THREE.Vector3();
        const quat = new THREE.Quaternion();

        // Ground
        pos.set( 0, - 0.5, 0 );
        quat.set( 0, 0, 0, 1 );
        const ground = createParalellepiped( 40, 1, 40, 0, pos, quat, new THREE.MeshPhongMaterial( { color: 0xFFFFFF } ) );
        ground.castShadow = true;
        ground.receiveShadow = true;
        textureLoader.load( "textures/floor.jpg", function ( texture ) {

          texture.wrapS = THREE.RepeatWrapping;
          texture.wrapT = THREE.RepeatWrapping;
          texture.repeat.set( 10, 10 );
          ground.material.map = texture;
          ground.material.needsUpdate = true;

        } );

        // walls
        const wallMaterial = new THREE.MeshPhongMaterial({
          color: 0x1982CC,
          transparent :true,
          opacity :0.1
        });
        pos.set( 0, 6, 0 );
        quat.set(0,0,0,1);
        const wall = createParalellepiped( 10, 0.1, 10, 0, pos, quat, wallMaterial );
        wall.castShadow = false;
        wall.receiveShadow = true;
        wall.userData.physicsBody.setFriction( 0.5 );
        wall.userData.physicsBody.setRestitution( 1);
        pos.set( -5, 2, 0 );
        quat.setFromAxisAngle( new THREE.Vector3( 0, 0, 1 ), 90 * Math.PI / 180 );
        const wall1 = createParalellepiped( 10, 0.1, 10, 0, pos, quat, wallMaterial );
        wall1.castShadow = false;
        wall1.receiveShadow = true;
        wall1.userData.physicsBody.setFriction( 0.5 );
        wall1.userData.physicsBody.setRestitution( 1);

        pos.set( 5, 2, 0 );
        const wall2 = createParalellepiped( 10, 0.1, 10, 0, pos, quat,wallMaterial );
        wall2.castShadow = false;
        wall2.receiveShadow = true;

        wall2.userData.physicsBody.setFriction( 0.5 );
        wall2.userData.physicsBody.setRestitution( 1);

        pos.set( 0, 2, -5 );
        quat.setFromAxisAngle( new THREE.Vector3( 0, 0, 1 ), 90 * Math.PI / 180 );
        const wall3 = createParalellepiped( 10, 10, 0.1, 0, pos, quat, wallMaterial );
        wall3.castShadow = false;
        wall3.receiveShadow = true;
        wall3.userData.physicsBody.setFriction( 0.5 );
        wall3.userData.physicsBody.setRestitution( 1);

        pos.set( 0, 2, 5 );
        const wall4 = createParalellepiped( 10, 10, 0.1, 0, pos, quat, wallMaterial );
        wall4.castShadow = false;
        wall4.receiveShadow = true;
        wall4.userData.physicsBody.setFriction( 0.5 );
        wall4.userData.physicsBody.setRestitution( 1);

        // table
				pos.set( 0, tableHeight / 2, 0 );
        quat.set(0,0,0,1);
				// quat.setFromAxisAngle( new THREE.Vector3( 0, 0, 1 ), 5 * Math.PI / 180 );
				table = createParalellepiped( tableLength, tableHeight, tableWidth, 0, 
          pos, quat, new THREE.MeshPhongMaterial( { 
            color: 0x606060,
            transparent :true,
            opacity :0.9
          } ) );
				table.castShadow = true;
				table.receiveShadow = true;
        table.userData.physicsBody.setFriction( 0.5 );
        table.userData.physicsBody.setRestitution( 1);
        
        // table split line
        const positions = [
          -tableLength/2, tableHeight + 0.01 , 0, 
          tableLength/2, tableHeight + 0.01 , 0];
        const colors = [0.8,0.8,1,1,1,1];
        const geometry = new THREE.BufferGeometry();
        geometry.setAttribute('position', new THREE.Float32BufferAttribute( positions, 3 ))
        geometry.setAttribute('color', new THREE.Float32BufferAttribute( colors, 3 ))
        const material = new THREE.LineBasicMaterial({
          vertexColors: true, 
        })
        const splitLine = new THREE.Line( geometry, material);

        scene.add(splitLine);
        // net
        pos.set( 0, tableHeight + netHeight / 2, 0 );
				// quat.setFromAxisAngle( new THREE.Vector3( 0, 0, 1 ), 5 * Math.PI / 180 );
				const net = createParalellepiped( netLength, netHeight, netWidth, 0, 
          pos, quat, new THREE.MeshBasicMaterial( { 
            color: 0x222222,
            transparent :true,
            opacity :1.0
          } ) );
          textureLoader.load( "textures/net.png", function ( texture ) {

          texture.wrapS = THREE.RepeatWrapping;
          texture.wrapT = THREE.RepeatWrapping;
          texture.repeat.set( 10, 1 );
          net.material.map = texture;
          net.material.needsUpdate = true;

          } );
        net.castShadow = true;
        net.receiveShadow = true;
        net.userData.physicsBody.setFriction( 0.5 );
        net.userData.physicsBody.setRestitution( 0.5 );

        // paddles
        // audio

        const audioLoader = new THREE.AudioLoader();

        const listener = new THREE.AudioListener();
        camera.add( listener );

        audioLoader.load( 'sounds/ping_pong.mp3',  ( buffer ) =>{
          audio = new THREE.PositionalAudio( listener );
          audio.setBuffer( buffer );
          addBall();
        });
      }

      function addBall(){
        const pos = new THREE.Vector3();
        const quat = new THREE.Quaternion();

        ping_pong = new THREE.Mesh( new THREE.SphereGeometry( ballRadius, 20, 20 ), ballMaterailNormal );
        ping_pong.castShadow = true;
        ping_pong.receiveShadow = true;
        const ping_pongShape = new Ammo.btSphereShape( ballRadius );
        ping_pongShape.setMargin( margin );
        pos.set( params.startPositionX, params.startPositionY, params.startPositionZ );
        quat.set( 0, 0, 0, 1 );
        createRigidBody( ping_pong, ping_pongShape, ballMass, pos, quat );
        ping_pong.userData.physicsBody.setFriction( 0.5 );
        ping_pong.userData.physicsBody.setRestitution( 0.8 );
        // 设置初始速度
        ping_pong.userData.physicsBody.setLinearVelocity(
          new Ammo.btVector3(params.startLinearVelocityX, params.startLinearVelocityY, params.startLinearVelocityZ)
        );
        ping_pong.userData.physicsBody.setAngularVelocity(
          new Ammo.btVector3(params.startAngularVelocityX, params.startAngularVelocityY, params.startAngularVelocityZ)
        );

        ping_pong.userData.physicsBody.applyImpulse(
          new Ammo.btVector3(params.startImpulsX, params.startImpulsY, params.startImpulsZ),
          new Ammo.btVector3(0,0,0)
        )

        ping_pong.add( audio );
        
        ping_pong.userData.isServe = true;

        ping_pong.userData.player1Hitable = true;
        ping_pong.userData.player1Hited = false;
        ping_pong.userData.player2Hitable = false;
        ping_pong.userData.player2Hited = true;

        ping_pong.userData.isUp = false;
        ping_pong.userData.isDown = true;
      }

      function delBall(){
        physicsWorld.removeRigidBody(ping_pong.userData.physicsBody);
        scene.remove(ping_pong);
      }

      function createParalellepiped( sx, sy, sz, mass, pos, quat, material ) {

        const threeObject = new THREE.Mesh( new THREE.BoxGeometry( sx, sy, sz, 1, 1, 1 ), material );
        const shape = new Ammo.btBoxShape( new Ammo.btVector3( sx * 0.5, sy * 0.5, sz * 0.5 ) );
        shape.setMargin( margin );

        createRigidBody( threeObject, shape, mass, pos, quat );

        return threeObject;

      }

      function createRigidBody( threeObject, physicsShape, mass, pos, quat ) {

        threeObject.position.copy( pos );
        threeObject.quaternion.copy( quat );

        const transform = new Ammo.btTransform();
        transform.setIdentity();
        transform.setOrigin( new Ammo.btVector3( pos.x, pos.y, pos.z ) );
        transform.setRotation( new Ammo.btQuaternion( quat.x, quat.y, quat.z, quat.w ) );
        const motionState = new Ammo.btDefaultMotionState( transform );

        const localInertia = new Ammo.btVector3( 0, 0, 0 );
        physicsShape.calculateLocalInertia( mass, localInertia );

        const rbInfo = new Ammo.btRigidBodyConstructionInfo( mass, motionState, physicsShape, localInertia );
        const body = new Ammo.btRigidBody( rbInfo );

        threeObject.userData.physicsBody = body;

        scene.add( threeObject );

        if ( mass > 0 ) {

          rigidBodies.push( threeObject );

          // Disable deactivation
          body.setActivationState( 4 );

        }

        physicsWorld.addRigidBody( body );

      }

      function createMaterial() {

        return new THREE.MeshPhongMaterial( { color: createRandomColor() } );

      }

      function initInput() {

        window.addEventListener( 'keydown', function ( event ) {


        } );

        window.addEventListener( 'keyup', function () {

        } );

      }

      function setupGUI(){
        let gui = new GUI();
        gui.domElement.style.position = 'absolute';
        gui.domElement.style.top = getComputedStyle(document.documentElement).getPropertyValue('--menubar-height');
        gui.domElement.style.right = '0px';
        
        let startPosition = gui.addFolder('初始位置');
        startPosition.add(params, 'startPositionX', -3, 3, 0.1);
        startPosition.add(params, 'startPositionY', -3, 3, 0.1);
        startPosition.add(params, 'startPositionZ', -3, 3, 0.1);
        // startPosition.open();

        let startVelocity= gui.addFolder('初始速度');
        startVelocity.add(params, 'startLinearVelocityX', -10, 10, 0.1).name("线速度-X");
        startVelocity.add(params, 'startLinearVelocityY', -10, 10, 0.1).name("线速度-Y");
        startVelocity.add(params, 'startLinearVelocityZ', -10, 10, 0.1).name("线速度-Z");

        startVelocity.add(params, 'startAngularVelocityX', -360, 360, 0.1).name("角速度-X");
        startVelocity.add(params, 'startAngularVelocityY', -360, 360, 0.1).name("角速度-Y");
        startVelocity.add(params, 'startAngularVelocityZ', -360, 360, 0.1).name("角速度-Z");
        // startVelocity.open();

        let startImpuls= gui.addFolder('初始冲量');
        startImpuls.add(params, 'startImpulsX', -2, 2, 0.01);
        startImpuls.add(params, 'startImpulsY', -2, 2, 0.01);
        startImpuls.add(params, 'startImpulsZ', -2, 2, 0.01);

        // startImpuls.open();
        let serve= gui.addFolder('发球参数');
        serve.add(params, 'serveX', -1, 1, 0.001);
        serve.add(params, 'serveY', -1, 1, 0.001);
        serve.add(params, 'serveZ', -1, 1, 0.001);
        serve.add(params, 'servePosX', -2, 2, 0.01);
        serve.add(params, 'servePosY', -2, 2, 0.01);
        serve.add(params, 'servePosZ', -2, 2, 0.01);
        serve.add(params, 'force');
        // 
        let force= gui.addFolder('击打参数');
        const useImpulse = force.add(params, 'useImpulse').name('冲量控制').onChange((val)=>{
          if(val){
            useImpulse.name('速度控制');
          }
          else{
            useImpulse.name('冲量控制');
          }
        });
        takeTime = force.add(params, 'time', 0.1, 3, 0.01);

        force.add(params, 'force');
        force.open();
      }
      
      function onWindowResize() {

        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize( window.innerWidth, window.innerHeight );

        cameraPos.aspect = monitorPos.clientWidth / monitorPos.clientHeight;
        cameraPos.updateProjectionMatrix();
        cameraLV.aspect = monitorLV.clientWidth / monitorLV.clientHeight;
        cameraLV.updateProjectionMatrix();
        cameraAV.aspect = monitorAV.clientWidth / monitorAV.clientHeight;
        cameraAV.updateProjectionMatrix();

        rendererPos.setSize(monitorPos.clientWidth, monitorPos.clientHeight);
        rendererLV.setSize(monitorLV.clientWidth, monitorLV.clientHeight);
        rendererAV.setSize(monitorAV.clientWidth, monitorAV.clientHeight);

      }

      function animate() {

        requestAnimationFrame( animate );

        render();
        stats.update();

      }

      function render() {

        const deltaTime = clock.getDelta() / simScale;

        if(!isStop && !isPause) updatePhysics( deltaTime );

        // renderer.render( scene, camera );

        renderer.setViewport( 0, 0,  window.innerWidth, window.innerHeight );
        renderer.render( scene, camera ); 

        updateMonitor(deltaTime);
        // inset scene  
        monitorTime += deltaTime * 1000;

        // renderer.setScissorTest( true );
        // renderer.setScissor( 20, 20, 250, 250 );
        // renderer.setViewport( 20, 20, 250, 250 );
        // renderer.render( sceneMonitorLV, cameraMonitor );

        // renderer.setScissorTest( true );
        // renderer.setScissor( 20, 280, 250, 250 );
        // renderer.setViewport( 20, 280, 250, 250 );
        // renderer.render( sceneMonitorAV, cameraMonitor );
        // renderer.setScissorTest( false );

        
      }

      function updateMonitor(deltaTime){
        monitorTime += deltaTime * 1000;

        if((!isStop && !isPause) && monitorTime > 10){
          // 记录一次速度

          for(var i = 2, il = datPosX.length - 3; i < il; i+=3){
            datPosX[i] = datPosX[i + 3];
            datPosY[i] = datPosY[i + 3];
            datPosZ[i] = datPosZ[i + 3];
          }

          for(var i = 2, il = datLVX.length - 3; i < il; i+=3){
            datLVX[i] = datLVX[i + 3];
            datLVY[i] = datLVY[i + 3];
            datLVZ[i] = datLVZ[i + 3];
          }
          for(var i = 2, il = datAVX.length - 3; i < il; i+=3){
            datAVX[i] = datAVX[i + 3];
            datAVY[i] = datAVY[i + 3];
            datAVZ[i] = datAVZ[i + 3];
          }

          const lv = ping_pong.userData.physicsBody.getLinearVelocity();
          const av = ping_pong.userData.physicsBody.getAngularVelocity();

          datPosX[datPosX.length - 1] = ping_pong.position.x * 12;
          datPosY[datPosY.length - 1] = ping_pong.position.y * 20;
          datPosZ[datPosZ.length - 1] = ping_pong.position.z * 30;

          datLVX[datLVX.length - 1] = lv.x() * 5;
          datLVY[datLVY.length - 1] = lv.y() * 5;
          datLVZ[datLVZ.length - 1] = lv.z() * 5;

          datAVX[datAVX.length - 1] = av.x() / 36 + 20;
          datAVY[datAVY.length - 1] = av.y() / 36;
          datAVZ[datAVZ.length - 1] = av.z() / 36 - 20;

          // 防止同时修改值造成的渲染卡顿
          switch(updateDataIndex){
            case 0:
            monitorPosX.geometry.setPositions(datPosX);break;
            case 1:
            monitorPosY.geometry.setPositions(datPosY);break;
            case 2:
            monitorPosZ.geometry.setPositions(datPosZ); break;
            case 4:
            monitorLVX.geometry.setPositions( datLVX );break;
            case 5:
            monitorLVY.geometry.setPositions( datLVY );break;
            case 6:
            monitorLVZ.geometry.setPositions( datLVZ );break;
            case 7:
            monitorAVX.geometry.setPositions( datAVX );break;
            case 8:
            monitorAVY.geometry.setPositions( datAVY );break;
            case 9:
            monitorAVZ.geometry.setPositions( datAVZ );break;
            default:
              break;
          }
          monitorTime = 0;
          updateDataIndex = (updateDataIndex + 1) % 10;
        }

        rendererPos.render(scenePos, cameraPos);
        rendererLV.render(sceneLV, cameraLV);
        rendererAV.render(sceneAV, cameraAV);
      }

      function updatePhysics( deltaTime ) {

        // Step world
        physicsWorld.stepSimulation( deltaTime );

        // Update rigid bodies
        for ( let i = 0, il = rigidBodies.length; i < il; i ++ ) {

          const objThree = rigidBodies[ i ];
          const objPhys = objThree.userData.physicsBody;
          const ms = objPhys.getMotionState();
          if ( ms ) {

            ms.getWorldTransform( transformAux1 );
            const p = transformAux1.getOrigin();
            const q = transformAux1.getRotation();
            objThree.position.set( p.x(), p.y(), p.z() );
            objThree.quaternion.set( q.x(), q.y(), q.z(), q.w() );

          }
        }
        // 速度监测
        const lv = ping_pong.userData.physicsBody.getLinearVelocity();
        const av = ping_pong.userData.physicsBody.getAngularVelocity();
        // 计算空气阻力
        // x 方向
        const C = 0.5;
        const ro = 1.29;
        const S = Math.PI*ballRadius*ballRadius;
        const fx = -0.5 * C * ro * S * lv.x() * Math.abs(lv.x());
        const fy = -0.5 * C * ro * S * lv.y() * Math.abs(lv.y());
        const fz = -0.5 * C * ro * S * lv.z() * Math.abs(lv.z());
        ping_pong.userData.physicsBody.applyForce(new Ammo.btVector3(fx, fy, fz));
        
        // 方向及可击打检测
        // todo: 判断乒乓球在球桌的哪一侧发生了碰撞，然后判定可击打条件
        if(lv.y() > 0.1 && ping_pong.userData.isDown && 
          ping_pong.position.y <= tableHeight + ballRadius * 4){

          audio.play();
          ping_pong.material = ballMaterailContact;

          if(ping_pong.userData.player2Hited){
            ping_pong.userData.player1Hitable = true;
          }
          if(ping_pong.userData.player1Hited){
            ping_pong.userData.player2Hitable = true;
          }
        }
        
        if(ping_pong.userData.isUp && ping_pong.position.y > tableHeight + ballRadius * 2){
          ping_pong.material = ballMaterailNormal;
        }

        ping_pong.userData.isDown = lv.y() < 0;
        ping_pong.userData.isUp = lv.y() > 0;

        // console.log(lv.x(), lv.y(), lv.z());
        // console.log(av.x(), av.y(), av.z());
        // 位置过低时删除小球

        if(ping_pong.position.y < ballRadius * 2 ||
        (Math.abs(ping_pong.position.x) > 8 || Math.abs(ping_pong.position.y) > 8
        || Math.abs(ping_pong.position.z) > 8)){
          delBall();
          addBall();
          return;
        }

        autoPlay();
        
        // var numManifolds=dispatcher.getNumManifolds();
        // let contactPos = new Ammo.btVector3(0,0,0);

        // for(let i=0; i<numManifolds; i++){
        //   var contactManifold=dispatcher.getManifoldByIndexInternal(i);
        //   var objA=contactManifold.getBody0();
        //   var objB=contactManifold.getBody1();
        //   var numContacts = contactManifold.getNumContacts();
        //   // 播放碰撞声音
        //   if(objB.ly === ping_pong.userData.physicsBody.ly &&
        //   objA.ly === table.userData.physicsBody.ly){

        //       var nowTime = clock.elapsedTime;
        //       // 防止重复播放声音
        //       if(nowTime - ping_pong.userData.lastedContactTime > 0.05){
        //         // ping_pong.material = ballMaterailContact; 
        //       }
              
        //       ping_pong.userData.lastedContactTime = clock.elapsedTime;

        //       // 记录碰撞点
        //       // for(var j=0; j<numContacts; j++)
        //       // {
        //       //     var pt = contactManifold.getContactPoint(j);
        //       //     if(pt.getDistance()<=0.0){
        //       //         var posA = pt.getPositionWorldOnA();
        //       //         var posB = pt.getPositionWorldOnB();

        //       //         // contactPos = posB;
        //       //         // console.log("A:",i, posA.x(), posA.y(), posA.z()); // 碰撞点
        //       //         console.log("B:",i, posB.x(), posB.y(), posB.z());
        //       //     }
        //       // }
              
        //   }
        // }
        
      }

      function autoPlay(){
        
        const lv = ping_pong.userData.physicsBody.getLinearVelocity();
        const av = ping_pong.userData.physicsBody.getAngularVelocity();
        const pos = ping_pong.position;

        // 击打策略
        var lvx = lv.x();
        var lvy = lv.y();
        var lvz = lv.z();
        var avx = av.x();
        var avy = av.y();
        var avz = av.z();

        var heightFromTable = pos.y - tableHeight;
        var lengthToGo      = Math.abs(pos.x) + tableHeight + params.distance;
        var tt = Math.random() * 0.2 + 0.32;
        var tax = Math.random() * 360;
        var taz = Math.random() * 360 - 180;

        if(ping_pong.userData.player1Hitable && ping_pong.userData.isDown){
          ping_pong.userData.player1Hitable = false;
          ping_pong.userData.player1Hited = true;
          ping_pong.userData.player2Hited = false;
          console.log(`Player 1 Hit:`);
          hit(-1);
        }
        
        else if(ping_pong.userData.player2Hitable && ping_pong.userData.isDown){
          ping_pong.userData.player2Hitable = false;
          ping_pong.userData.player2Hited = true;
          ping_pong.userData.player1Hited = false;
          console.log(`Player 2 Hit: `);
          hit(1);
        }
      }
      
      function hit(direction){

        const lv = ping_pong.userData.physicsBody.getLinearVelocity();
        const av = ping_pong.userData.physicsBody.getAngularVelocity();
        const pos = ping_pong.position;

        // 击打策略
        var lvx = lv.x();
        var lvy = lv.y();
        var lvz = lv.z();
        var avx = av.x();
        var avy = av.y();
        var avz = av.z();          

        var heightFromTable = pos.y - tableHeight;
        var lengthToGo      = Math.abs(pos.x) + tableHeight + params.distance;

        console.log(`距离：${heightFromTable.toFixed(5)},${lengthToGo.toFixed(5)}\r\n`,
          `位置：${pos.x.toFixed(5)},${pos.y.toFixed(5)},${pos.z.toFixed(5)}\r\n`,
          `线速度：${lvx.toFixed(5)},${lvy.toFixed(5)},${lvz.toFixed(5)},\r\n`,
          `角速度：${avx.toFixed(1)},${avy.toFixed(1)},${avz.toFixed(1)},`);
        
        var tt = Math.random() * 0.2 + 0.32;
        var tax = Math.random() * 360;
        var taz = Math.random() * 360 - 180;

        var tvx = direction * lengthToGo / tt;
        var tvy = gravityConstant*-0.5*tt - heightFromTable / tt;

        takeTime.setValue(tt);


        if(params.useImpulse){
          // 计算当前冲量 v * mass
          var px = (tvx - lvx) * ballMass;
          var py = (tvy - lvy) * ballMass;
          var pz = 0;
          // 随机击打位置
          // todo: 根据当前位置，计算击打位置
          var hx = 0;
          var hy = 0;
          var hz = 0;
    
          ping_pong.userData.physicsBody.applyImpulse(
            new Ammo.btVector3(px, py, pz),
            new Ammo.btVector3(hx, hy, hz),
          );
        }
        else{
          ping_pong.userData.physicsBody.setLinearVelocity(
            new Ammo.btVector3(tvx, tvy, 0)
          )

          if(pos.z > 0){
            tax *= -1;
          }
          ping_pong.userData.physicsBody.setAngularVelocity(
            new Ammo.btVector3(tax, 0, taz)
          )
        }
        ping_pong.material = ballMaterailHit;
      }
    </script>

    <div class="Panel">
      <div id="menubar">
        <div class="menu">
          <div class="title" id="start">启动</div>
        </div>
        <div class="menu">
          <div class="title" id="pause">暂停</div>
        </div>
        <div class="menu">
          <div class="title" id="stop">停止</div>
        </div>
        <div class="menu">
          <div class="title">仿真速度</div>
          <div class="options">
            <div class="option" onclick="setSimScale(1)">/1</div>
            <div class="option" onclick="setSimScale(2)">/2</div>
            <div class="option" onclick="setSimScale(5)">/5</div>
          </div>
        </div>
        <div class="menu">
          <div class="title">切换主题</div>
          <div class="options">
            <div class="option" onclick="setTheme('blue')">蓝色</div>
            <div class="option" onclick="setTheme('dark')">暗色</div>
            <div class="option" onclick="setTheme('light')">亮色</div>
          </div>
        </div>
        <div class="menu right">
          版本：1.0
        </div>
      </div>
    </div>
  </body>
</html>